Gitea is a self-hosted Git server for teams that want repository hosting, pull requests, issues, releases, packages, webhooks, and other development workflows on infrastructure they control. In this tutorial, you will deploy Gitea on a Raff Technologies Linux VM with Docker Compose, PostgreSQL, Nginx, HTTPS, and SSH clone access. The web service stays bound to localhost behind Nginx, while Git-over-SSH uses a separate host port so it does not conflict with the VM's administrative SSH service.
This guide follows Gitea's current Docker deployment model and uses the official docker.gitea.com/gitea image. At the time of this update, Gitea's official installation documentation uses the 1.27.3 release in its stable Docker examples. The official docs also recommend choosing a database early rather than relying on SQLite when you expect the instance to grow; this tutorial uses PostgreSQL for a multi-user setup. See the Gitea Docker installation documentation and database preparation guidance for the upstream reference.
You need Ubuntu 24.04, Docker with Compose v2, Nginx, a domain such as git.example.com, SSH access with sudo privileges, and DNS pointing the domain to the VM.
Step 1 — Confirm Docker, DNS, and the Ubuntu Host
Check the Ubuntu release:
cat /etc/os-release
Confirm Docker and Compose are available:
docker --version docker compose version
Check that your Gitea hostname resolves to the VM:
dig +short A git.example.com
If Docker is not installed yet, follow How to Install Docker on Ubuntu 24.04. If Nginx is missing, use How to Install Nginx on Ubuntu 24.04.
Verify: Ubuntu should report 24.04, docker compose version should work, and the DNS A record should return the VM's public IPv4 address.
Step 2 — Create the Gitea Project and Protect Database Secrets
Create a dedicated project directory:
mkdir -p ~/gitea cd ~/gitea
Generate a strong PostgreSQL password:
openssl rand -hex 32
Create an environment file:
nano .env
Add the generated password:
POSTGRES_PASSWORD=replace_with_your_generated_password
Restrict the file so other local users cannot read it:
chmod 600 .env
Do not commit this .env file to a Git repository.
Verify: Run stat -c '%a %n' .env. It should show permission mode 600 for .env.
Step 3 — Create the Docker Compose Stack
Create the Compose file:
nano compose.yaml
Add:
services: gitea: image: docker.gitea.com/gitea:1.27.3 container_name: gitea restart: unless-stopped environment: USER_UID: "1000" USER_GID: "1000" GITEA__database__DB_TYPE: postgres GITEA__database__HOST: db:5432 GITEA__database__NAME: gitea GITEA__database__USER: gitea GITEA__database__PASSWD: ${POSTGRES_PASSWORD} GITEA__server__DOMAIN: git.example.com GITEA__server__ROOT_URL: https://git.example.com/ GITEA__server__SSH_DOMAIN: git.example.com GITEA__server__SSH_PORT: "2222" volumes: - gitea_data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "127.0.0.1:3000:3000" - "2222:22" depends_on: db: condition: service_healthy db: image: postgres:16-alpine container_name: gitea-db restart: unless-stopped environment: POSTGRES_USER: gitea POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: gitea volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"] interval: 10s timeout: 5s retries: 5 volumes: gitea_data: postgres_data:
Replace every git.example.com value with your real hostname. Gitea supports PostgreSQL 12 and newer, so PostgreSQL 16 is within the supported range. The web port is published only on 127.0.0.1; Nginx will be the public HTTP entry point. Host port 2222 carries Git-over-SSH and stays separate from the VM's normal SSH port 22.
Check the rendered Compose configuration without printing secret values into a public log:
docker compose config --services
Verify: The command should list exactly gitea and db, and docker compose config -q should exit without a validation error.
Step 4 — Start Gitea and PostgreSQL
Start the stack:
docker compose up -d
Check container state:
docker compose ps
Review recent logs:
docker compose logs --tail=50 gitea docker compose logs --tail=50 db
Test the local Gitea HTTP endpoint:
curl -I http://127.0.0.1:3000
The first startup may take longer while the database initializes and Gitea prepares its data directory.
Verify: docker compose ps should show the database healthy and Gitea running, while curl -I http://127.0.0.1:3000 should return an HTTP response.
Step 5 — Configure Nginx as the Public Reverse Proxy
Create an Nginx server block:
sudo nano /etc/nginx/sites-available/git.example.com
Add:
server { listen 80; listen [::]:80; server_name git.example.com; location / { client_max_body_size 512M; proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Connection $http_connection; proxy_set_header Upgrade $http_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; } }
Gitea's reverse-proxy guidance requires the public ROOT_URL to match the external URL and expects the proxy to preserve the host and forwarded protocol. The upload-size limit prevents Nginx from rejecting larger web uploads with 413 Request Entity Too Large. See the official Gitea reverse proxy documentation.
Enable the site:
sudo ln -s /etc/nginx/sites-available/git.example.com \ /etc/nginx/sites-enabled/git.example.com
Test and reload Nginx:
sudo nginx -t sudo systemctl reload nginx
Verify: sudo nginx -t should report successful syntax, and curl -I http://git.example.com should reach the Gitea site through Nginx.
Step 6 — Enable HTTPS and Open Only the Required Public Ports
If UFW is active, first confirm administrative SSH is allowed:
sudo ufw status sudo ufw allow OpenSSH
Allow HTTP/HTTPS and the Gitea SSH clone port:
sudo ufw allow 'Nginx Full' sudo ufw allow 2222/tcp
Install Certbot and request a certificate:
sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx --redirect -d git.example.com
Test renewal:
sudo certbot renew --dry-run
Do not expose container port 3000 publicly; it should remain bound to localhost.
Verify: curl -I https://git.example.com should complete over HTTPS, HTTP should redirect to HTTPS, and sudo ss -ltnp should show Gitea's web port bound to 127.0.0.1:3000 rather than 0.0.0.0:3000.
Step 7 — Complete the Gitea Installation and Create the First Administrator
Open:
https://git.example.com
The Docker environment variables already provide the PostgreSQL connection values and public server URL. Confirm the database host is db:5432, database name is gitea, and the public site URL matches your HTTPS hostname.
Create the initial administrator account with a strong unique password. After the installation finishes, sign in and review the site administration settings.
For a private team instance, disable open registration after creating the accounts you need. Edit compose.yaml and add this environment variable to the gitea service:
GITEA__service__DISABLE_REGISTRATION: "true"
Apply the change:
cd ~/gitea docker compose up -d gitea
Then open the sign-up page in a private browser session and confirm public registration is no longer available. Also require multi-factor authentication for administrator accounts and keep a separate day-to-day user account instead of using the administrator identity for normal Git work.
Verify: You should be able to sign in as the administrator over HTTPS, reach the Site Administration area without a certificate warning, and confirm that open self-registration is disabled for a private team deployment.
Step 8 — Configure and Test Git-over-SSH
In Gitea, add your public SSH key under your user settings. From your workstation, test the dedicated Git SSH port:
ssh -T -p 2222 [email protected]
The connection may report that interactive shell access is disabled; that is expected for a Git service. The important result is that the connection reaches Gitea rather than the VM's normal SSH daemon.
If port 2222 is not reachable, check:
sudo ufw status docker compose ps sudo ss -ltnp | grep 2222
Verify: ssh -T -p 2222 [email protected] should reach the Gitea SSH service associated with your account.
Step 9 — Create a Repository and Push the First Commit
Create a repository in the Gitea web interface, for example demo-repo. Then clone it from your workstation using the SSH URL shown by Gitea:
git clone ssh://[email protected]:2222/your_username/demo-repo.git cd demo-repo
Create a file and push a commit:
printf '# Gitea test repository\n' > README.md git add README.md git commit -m "Add README" git push origin main
If the repository was created with a different default branch, use the branch name shown by Gitea instead of assuming main.
Verify: Refresh the repository in the browser. The new commit and README should appear, proving that SSH authentication, repository storage, Git receive hooks, and the database are working together.