Skip to content
Guides · Recipes

How to Run Gitea in a Sandbox and Push to It Over HTTPS

Updated October 27, 2026 .md

Short answer: Run the official gitea/gitea Docker image in a Sandboxer microVM with SQLite and --restart unless-stopped, then host port 3000 privately. Git can't use the ?_token= link, so give git the __sbx_auth cookie with http.extraHeader and use a Gitea access token as the password. Push and clone then work over HTTPS.

What we ran#

Everything below ran on production on October 27, 2026, in one small sandbox (2 vCPU, 4 GB RAM, 12 GB disk, $0.018/hour) with Gitea 1.24.7 from the gitea/gitea:1.24 image and git 2.53 on the laptop side. Tokens are replaced with <token>. Times are single runs unless the table says otherwise.

Step Result
sbx new --type small 3.2 s to ready
Pull gitea/gitea:1.24 and start it 7.5 s to a passing /api/healthz
First push (one commit) over the private URL 0.76 s
Mirror push of expressjs/express (10.8 MB pack, 329 branches and tags) 28.9 s
Push a 50 MB commit of random data 6.1 s and 6.9 s (two runs)
Clone express back (6,183 commits) 6.5 s
sbx stop --wait, then sbx resume 2.2 s and 1.3 s, memoryRestored: true, next push 0.73 s

1. Start Gitea#

Docker is preinstalled in every sandbox. We followed Gitea's Docker install guide and set the config with its GITEA__section__KEY environment variables, so no web installer runs:

sbx new --name gitea --type small --no-env
id=sb_o4aq9kxinn    # the id that sbx new printed; its subdomain was rich-curious-trout

sbx exec $id --timeout 600 "docker run -d --name gitea --restart unless-stopped \
  -p 3000:3000 -v gitea-data:/data \
  -e GITEA__database__DB_TYPE=sqlite3 \
  -e GITEA__security__INSTALL_LOCK=true \
  -e GITEA__server__ROOT_URL=https://rich-curious-trout-3000.box.sandboxer.com/ \
  -e GITEA__server__DISABLE_SSH=true \
  -e GITEA__service__DISABLE_REGISTRATION=true \
  gitea/gitea:1.24 && until curl -fs localhost:3000/api/healthz >/dev/null; do sleep 0.5; done"
sbx exec $id "docker exec gitea gitea --version"
Gitea version 1.24.7 built with GNU Make 4.4.1, go1.24.9 : bindata, timetzdata, sqlite, sqlite_unlock_notify

What each setting does here:

  • ROOT_URL is the hosted URL you get in step 2: https://<subdomain>-3000.box.sandboxer.com/. Gitea uses it for the clone URLs it shows and returns from its API. The subdomain is in the sbx new output, and it stays the same across stop and resume.
  • INSTALL_LOCK=true skips the first-run web installer. Without it, the first person to open the URL configures the server.
  • DISABLE_REGISTRATION=true means only accounts you create exist.
  • DISABLE_SSH=true hides SSH clone URLs. Everything in this guide uses HTTPS.
  • -v gitea-data:/data keeps repositories, the SQLite database and config in a named volume on the sandbox disk.

Gitea needs --timeout 600 on the first sbx exec only because of the image pull. sbx exec stops a command after 30 seconds by default.

2. Create a user and a token, then host the port#

Create the admin account and an access token with Gitea's admin CLI. --raw prints only the token, so you can write it straight to a file on your laptop:

sbx exec $id "docker exec -u git gitea gitea admin user create --admin \
  --username alice --password '<password>' --email alice@example.com --must-change-password=false"
sbx exec $id "docker exec -u git gitea gitea admin user generate-access-token \
  --username alice --token-name laptop --scopes write:repository,write:user --raw" > gitea_token
New user 'alice' has been successfully created!

Now give port 3000 a URL. Hosted URLs are private by default:

sbx host $id 3000 --title Gitea > host.json
python3 -c "import json; print(json.load(open('host.json'))['url'])" | cut -d= -f2 > sbx_token
{"ok":true,"type":"port.hosted","sandboxId":"sb_o4aq9kxinn","port":3000,"url":"https://rich-curious-trout-3000.box.sandboxer.com?_token=<token>", …}

You now hold two secrets, and they do different jobs:

Secret Who checks it What it protects
Hosted URL token (sbx_token) The Sandboxer proxy, before Gitea sees anything Whether a request reaches Gitea at all
Gitea access token (gitea_token) Gitea Which user you are and which repos you can push to

A browser opens the ?_token= link once, gets a cookie and is in. Git can't do that. It appends /info/refs?service=… to the repository URL, and it doesn't keep cookies between commands. We tried three ways from the laptop:

B=https://rich-curious-trout-3000.box.sandboxer.com
GIT_TERMINAL_PROMPT=0 git ls-remote $B/alice/demo.git
GIT_TERMINAL_PROMPT=0 git ls-remote "$B/alice/demo.git?_token=<token>"
curl -s -w " %{http_code}\n" -H "Cookie: __sbx_auth=$(cat sbx_token)" $B/api/v1/version
fatal: could not read Username for 'https://rich-curious-trout-3000.box.sandboxer.com': terminal prompts disabled
fatal: could not read Username for 'https://rich-curious-trout-3000.box.sandboxer.com': terminal prompts disabled
{"version":"1.24.7"}
 200

The first two got 401 from the proxy, and git read that as "Gitea wants a password". The token in the query string didn't help. What works is the __sbx_auth cookie, which is what the proxy sets after a browser opens the token link. Its value is the URL's token.

4. Configure git and push#

Git's http.<url>.extraHeader sends a header with every request to one URL prefix, so only this server gets the cookie. The Gitea token goes in the credential store as the password:

git config --global http.$B/.extraHeader "Cookie: __sbx_auth=$(cat sbx_token)"
git config --global credential.helper store
printf 'protocol=https\nhost=rich-curious-trout-3000.box.sandboxer.com\nusername=alice\npassword=%s\n\n' \
  "$(cat gitea_token)" | git credential approve

credential.helper store keeps the token in plain text in ~/.git-credentials. Use your OS keychain helper instead if you have one. Either way, the cookie sits in plain text in ~/.gitconfig, so treat that file like a password too.

Create a private repository through Gitea's API (same two credentials) and push:

curl -s -u "alice:$(cat gitea_token)" -H "Cookie: __sbx_auth=$(cat sbx_token)" \
  -H 'Content-Type: application/json' -d '{"name":"demo","private":true}' $B/api/v1/user/repos
mkdir demo && cd demo && git init -q && echo "# demo" > README.md
git add . && git commit -qm "first commit"
git remote add origin $B/alice/demo.git
git push -u origin main
remote: . Processing 1 references
remote: Processed 1 references in total
To https://rich-curious-trout-3000.box.sandboxer.com/alice/demo.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

That push took 0.76 seconds. In a browser, open the full ?_token= link once, then sign in to Gitea as alice.

5. Tell the two 401s apart#

When a push fails, the message tells you which layer said no. These are the outputs we got:

What's wrong git prints
No cookie, no stored credentials fatal: could not read Username for '…'
No cookie, correct Gitea token stored fatal: Authentication failed for '…' (no remote: line)
Cookie right, Gitea token wrong remote: Failed to authenticate user then fatal: Authentication failed
Cookie right, no credentials, private repo fatal: could not read Username for '…'

A remote: line means the request reached Gitea, so the cookie is fine and the Gitea token is the problem. No remote: line with a stored token means the proxy rejected the cookie.

Watch out for the second row. After an authentication failure, git tells the credential helper to forget the password it just tried. When our cookie was missing, the proxy's 401 made git erase the correct Gitea token from ~/.git-credentials (the file was 0 bytes afterwards), and the next push failed even with the cookie fixed. Fix the cookie first, then run the git credential approve line from step 4 again.

6. A real repository: speed#

We mirrored a real project to see how it behaves with many refs and some history:

git clone -q --bare https://github.com/expressjs/express.git express.git
cd express.git && time git push -q --mirror $B/alice/express.git
real	0m28.866s

The pack was 10.8 MB, but there were 329 branches and tags, and Gitea processes every pushed ref (the Processing 329 references line). A 50 MB commit of random data, one ref, pushed in 6.1 and 6.9 seconds. Cloning express back took 6.5 seconds and had all 6,183 commits and 306 tags.

Downloads depend on your distance from the sandbox. Cloning the 96 MB demo repo took 58 seconds from our laptop. The same clone inside the sandbox over localhost took 5.4 seconds. A 50 MB download through the hosted URL took 14 to 29 seconds, and the same size from a Hetzner speed-test server in Helsinki took 21 seconds from the same laptop. Our sandbox ran in eu-central. So the network path set our clone speed, not the proxy. If a job clones a lot, run it in a sandbox and clone over localhost:3000.

7. What survives a stop and resume#

sbx stop $id --wait
sbx resume $id
sbx exec $id "docker ps --format '{{.Names}} {{.Status}}'"
gitea Up About a minute

Stop took 2.2 seconds and resume 1.3 seconds. The resume reported memoryRestored: true, so Gitea never restarted. The hosted URL and its token were unchanged, the old cookie still worked, and the next push took 0.73 seconds.

A resume boots from disk instead when no machine with the same CPU type is free, or when you resize. Then the gitea-data volume is there and the restart policy starts Gitea again. We didn't force that here. Restarting the Docker daemon shows the same policy at work:

sbx exec $id --timeout 120 "sudo systemctl restart docker
until curl -fs localhost:3000/api/healthz >/dev/null; do sleep 0.5; done
docker ps --format '{{.Names}} {{.Status}}'; docker exec gitea ls /data/git/repositories/alice"
gitea Up Less than a second
demo.git
express.git

8. Public instead of private#

sbx host $id 3000 --public drops the proxy's check, and Gitea's own login is the only gate:

Request with a public URL Result
curl $B/api/v1/version, anonymous 200, {"version":"1.24.7"}
git ls-remote of the private repo, anonymous fatal: could not read Username (Gitea asked for credentials)
Same, with alice and the Gitea token Lists HEAD

Public is simpler for CI runners and teammates: one credential, no cookie. It also exposes Gitea's login page to anyone who finds the URL. Switching back with sbx host $id 3000 --private minted a new token: the old cookie got 401 and the new one 200. Update http.<url>.extraHeader on every machine after a switch.

9. Clean up#

sbx delete $id --yes

A small sandbox costs $0.018/hour while it runs and nothing while it is stopped. Remove the extraHeader line and the stored credential from your laptop when you're done.

FAQ#

No. In our test, git ls-remote with ?_token= on the URL got the proxy's 401, because git adds its own path and query after the URL. Send the token as the __sbx_auth cookie with http.<url>.extraHeader instead.

Why did git ask for a username when my Gitea token was fine?#

The proxy answered 401 before the request reached Gitea, and git treats any 401 as a password prompt. Check that the cookie is set for the exact URL prefix and that you didn't switch the port public and back, which changes the token.

Can I push over SSH instead?#

We didn't test it. Gitea's SSH server listens inside the container, and a sandbox's SSH endpoint is the sandbox's own sshd, not Gitea. This guide sets DISABLE_SSH=true and uses HTTPS only.

Will my repositories survive if the sandbox auto-stops?#

Yes. Auto-stop is a normal stop: disk and memory are saved, and sbx resume brings Gitea back. The repos live in the gitea-data volume, and --restart unless-stopped covers the times a resume boots from disk.

Do I need a bigger size than small?#

Not for this. Gitea with SQLite handled a 10.8 MB mirror and two 50 MB pushes on small. Moving to a smaller size isn't supported yet (you'd create a new sandbox and copy), so start small and resize up on resume if you need to.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide