9.1 KiB
name, description, version, author, platforms, created_by, category, metadata
| name | description | version | author | platforms | created_by | category | metadata | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| gitea-self-hosted | Deploy and manage Gitea (self-hosted Git service) with Docker, nginx, Let's Encrypt, and VPS reverse proxy. | 1.1.0 | ray |
|
agent | self-hosting |
|
Gitea Self-Hosted Git Service
Deploy Gitea — a lightweight, self-hosted Git service — via Docker Compose with nginx reverse proxy, Let's Encrypt SSL, and optional VPS edge proxy through Tailscale.
When to use
- User wants a private, self-hosted Git forge (no GitHub dependency)
- User has a Docker-capable home server with storage for repos
- User wants git history and rollback for their projects (dev code, Docker configs, automation scripts)
Architecture
Browser → VPS (nginx SSL, tailscale) → Home nginx (SSL) → Gitea (Docker:3000)
Gitea runs in a single Docker container with SQLite backend. Data persists on storage drive.
Docker Compose
docker-compose.yml
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=gitea.graj-media.com
- GITEA__server__SSH_DOMAIN=gitea.graj-media.com
- GITEA__server__HTTP_PORT=3000
- GITEA__server__ROOT_URL=https://gitea.graj-media.com
- GITEA__server__DISABLE_SSH=true
- GITEA__server__LFS_START_SERVER=true
networks:
- gitea-net
networks:
gitea-net:
driver: bridge
Critical: Do NOT map port 22 for SSH. Gitea's internal SSH server conflicts with the container's system SSH daemon. Set DISABLE_SSH=true and START_SSH_SERVER=false to prevent crash loops. If SSH git protocol is needed, forward a different host port (e.g., "127.0.0.1:3022:22") and set SSH_PORT=3022.
Initial setup (skip the web installer)
Gitea's Docker entrypoint rewrites app.ini from environment variables on every restart, so the web install page is unreliable with env-var-based config. Set up via CLI instead:
# 1. Stop the running container
docker compose -f /path/to/docker-compose.yml stop
# 2. Generate secrets and write config
cat > /path/to/data/gitea/conf/app.ini << 'INI'
APP_NAME = Ray's Git
RUN_MODE = prod
[server]
APP_DATA_PATH = /data/gitea
DOMAIN = gitea.graj-media.com
SSH_DOMAIN = gitea.graj-media.com
HTTP_PORT = 3000
ROOT_URL = https://gitea.graj-media.com
DISABLE_SSH = true
LFS_START_SERVER = true
[database]
PATH = /data/gitea/gitea.db
DB_TYPE = sqlite3
[security]
INSTALL_LOCK = true
SECRET_KEY = <run: gitea generate secret SECRET_KEY>
[oauth2]
JWT_SECRET = <run: gitea generate secret JWT_SECRET>
[git]
LFS_JWT_SECRET = <run: gitea generate secret LFS_JWT_SECRET>
[service]
DISABLE_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = false
INI
# 3. Run database migration (as git user, UID 1000)
docker run --rm \
-v /path/to/data:/data \
--user 1000:1000 \
gitea/gitea:latest \
gitea migrate
# 4. Create admin user
docker run --rm \
-v /path/to/data:/data \
--user 1000:1000 \
gitea/gitea:latest \
gitea admin user create --username ray --password "<password>" --email ray@example.com --admin
# 5. Start the container
docker compose -f /path/to/docker-compose.yml up -d
API Token Workflow (Post-Deploy Automation)
After Gitea is running with an admin user, generate an API token for automation (repo creation, user management, etc.):
# Generate an API token via CLI (scoped to all)
TOKEN=$(docker exec -u git gitea gitea admin user generate-access-token \
--username ray --token-name "automation" --scopes "all" 2>&1 | grep -v "^$")
echo "Token: $TOKEN"
Create repos via the API
TOKEN="<token-from-above>"
BASE="http://127.0.0.1:3000/api/v1"
curl -s -X POST "$BASE/user/repos" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"my-repo","private":false,"auto_init":false}'
All repos start empty. Push code after creation (see below).
Push code to a new repo
When pushing over HTTP with credentials in the URL, URL-encode special characters:
# Example: password "4W#UxJ^acTrdPT" → encode # as %23 and ^ as %5E
# 4W%23UxJ%5EacTrdPT
cd /path/to/project
git init
git add -A
git commit -m "initial commit"
git remote add origin http://ray:4W%23UxJ%5EacTrdPT@127.0.0.1:3000/ray/my-repo.git
git branch -m master main # Gitea defaults to 'main' branch
git push -u origin main
Pitfall — default branch is master, not main: git init creates a master branch by default, but Gitea repos default to main. Rename before pushing: git branch -m master main.
Pitfall — must_change_password blocks API access: When the admin user was created via gitea admin user create, the user record may have must_change_password=1 set. This causes ALL API calls (even with a valid token) to return:
{"message":"You must change your password. Change it at: ..."}
Fix — clear the flag directly in SQLite (no restart needed):
docker exec gitea sqlite3 /data/gitea/gitea.db \
"UPDATE user SET must_change_password=0 WHERE name='ray';"
Then retry the API call.
Home server nginx
Gitea needs nginx reverse proxy on the home server:
server {
listen 443 ssl;
server_name gitea.graj-media.com;
ssl_certificate /etc/letsencrypt/live/grajmedia.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grajmedia.duckdns.org/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 512M;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
VPS reverse proxy (when using a VPS edge)
If the home server is behind a VPS (Tailscale tunnel), add a server block on the VPS:
server {
server_name gitea.graj-media.com;
client_max_body_size 512M;
location / {
proxy_pass https://100.93.253.36:443; # home server Tailscale IP
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_verify off;
proxy_read_timeout 86400;
proxy_buffering off;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/graj-media.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/graj-media.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
Then expand the VPS cert to include the new subdomain. See vps-reverse-proxy skill Step 6 for the incremental --expand pattern.
Pitfalls
- Docker entrypoint overrides app.ini on restart: Environment variables (in
GITEA__section__keyformat) take precedence. If you setDISABLE_SSH=falsevia env var, Gitea will try to bind port 22 inside the container, conflicting with the system SSH daemon. The container enters a crash loop: "bind: address already in use". Fix: setDISABLE_SSH=trueANDSTART_SSH_SERVER=falsevia env vars in docker-compose.yml. giteaCLI refusals as root: Gitea refuses to run as root. Use--user 1000:1000with ephemeral containers, ordocker exec -u git gitea gitea <command>on running containers.- app.ini provisioning order: If you write app.ini manually (with
INSTALL_LOCK=true) but the database hasn't been initialized yet, Gitea crashes at startup. Always rungitea migratebefore starting withINSTALL_LOCK=true. Conversely, ifINSTALL_LOCK=false, the entrypoint regenerates app.ini from env vars and ignores your manual edits. - SSH port mapping inside container: The
ports:directive"127.0.0.1:3022:22"maps host port 3022 to container port 22. But if DISABLE_SSH=false, Gitea's built-in SSH server competes with the container'ssshdfor port 22. Preferred: setDISABLE_SSH=trueand use HTTPS cloning only. - Logs showing "Unable to GetListener: bind: address already in use" means SSH port conflict. Either disable SSH or use a non-conflicting internal port via
SSH_LISTEN_PORT=<different>. - CSRF token on install page: When using the web installer (not CLI), the form submission POST may hang or time out when ROOT_URL is HTTPS but accessed over HTTP. Use the CLI path instead.
- Verify after deploy:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/should return 200. The login page athttps://gitea.graj-media.com/user/loginshould load without SSL warnings.