Gitea 28.2.0 on AWS User Guide
Overview
This guide covers launching and using the Gitea 28.2.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Apache Subversion (SVN) and CI/CD Tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Gitea is a lightweight, self hosted Git service written in Go. It gives a team Git hosting with a familiar web interface: repositories with branch protection and code review through pull requests, issues and milestones, a wiki per repository, releases, a package registry, organisations and teams, and Gitea Actions for CI. This image runs the official Gitea 28.2.0 release with a SQLite database on the instance, so there is no separate database server to run.
The cloudimg image adds:
- HTTPS from the first boot. nginx serves Gitea on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
- An administrator and secrets unique to your instance. First boot creates the database and the administrator
gitea_adminwith a password generated on your instance, and generates Gitea's secret key, internal token and token signing secrets. Nothing secret is baked into the image; the settings file holds no secret at all. - Private by default. Sign in is required to see anything, visitors cannot register their own accounts (the administrator creates them), and Gitea's web installer is locked.
- Fail closed. Gitea refuses to start until first boot has finished, and so does nginx.
- Gitea on loopback only. Gitea's web server listens on
127.0.0.1:3000behind nginx. Git over SSH uses Gitea's built in SSH server on port 2222 (key authentication only, no shell), so port 22 stays your administrator SSH. The open ports are 22, 80, 443 and 2222. - Protection against server side request forgery. Webhooks, repository migrations and mirrors cannot reach private, loopback or link local addresses such as the instance metadata service, and Gitea and nginx are blocked from the metadata service at the kernel level. The AMI requires IMDSv2.
- Hardened defaults. Gitea runs as its own unprivileged
gituser in a systemd sandbox that can write only/var/lib/gitea. Its public address comes from your configuration, never from a request, and requests for a host name the instance does not serve are dropped. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Gitea 28.2.0 (the official gitea-28.2.0-linux-amd64 release binary, checksum verified), git, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (administrator SSH), TCP 443 (Gitea), TCP 80 (redirects to HTTPS) and TCP 2222 (git over SSH) from your own networks only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Gitea 28.2.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. Repositories and the database live under /var/lib/gitea on the root volume; size the root volume for the repositories you expect to host.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=50,VolumeType=gp3}' \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=gitea}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Gitea 28.2.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes a few seconds. Then the three units are active:
systemctl is-active gitea-firstboot gitea nginx
active
active
active
Gitea's web server listens on loopback only, its SSH server on 2222, and nginx serves ports 80 and 443. (The second loopback port is Gitea's internal proxy for outbound git operations.)
sudo ss -tlnp | awk 'NR==1 || /gitea|nginx|sshd/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN 127.0.0.1:44181 users:(("gitea"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 127.0.0.1:3000 users:(("gitea"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN *:2222 users:(("gitea"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
The version on the image:
gitea --version
gitea version 28.2.0 built with go1.27.2
Step 4: Get your URL and password
sudo cat /root/gitea-credentials.txt
The file holds the address Gitea is served as (your instance's public DNS name by default), the administrator user and the password generated on this instance, and the form of an SSH clone URL:
GITEA_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
GITEA_ADMIN_USER=gitea_admin
GITEA_ADMIN_PASSWORD=<24 letters and digits>
GITEA_SSH_PORT=2222
GITEA_SSH_CLONE=ssh://git@ec2-<public-ip-dashed>.<region>.compute.amazonaws.com:2222/<owner>/<repo>.git
Step 5: Sign in from your browser
Open GITEA_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or install your own certificate (Step 9). Sign in with gitea_admin and the password from Step 4.

The sign in page has no Register link: on this image only the administrator can create accounts. After signing in you see your dashboard, with your activity and your repositories:

To change the administrator password, open the user menu at the top right, Settings, Account.
Step 6: Create a repository and push to it
In the browser, choose + at the top right, New Repository, give it a name and choose Create Repository. The repository page shows the clone URLs under Code, for HTTPS and for SSH (port 2222):

From your workstation, push an existing project over HTTPS (git asks for your Gitea user name and password; while the certificate is self signed, tell git to accept it for this host only):
git -c http.sslVerify=false push https://<gitea-host>/<owner>/<repo>.git main
Or add your SSH public key in the browser (user menu, Settings, SSH / GPG Keys, Add Key) and use the SSH URL on port 2222:
git clone ssh://git@<gitea-host>:2222/<owner>/<repo>.git
You can try both from the instance itself. This creates a private repository called demo through Gitea's API; the password is handed to curl in a file, so it never appears on a command line:
H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
CFG=$(mktemp)
sudo sed -n 's/^GITEA_ADMIN_PASSWORD=\(.*\)/user = "gitea_admin:\1"/p' /root/gitea-credentials.txt > "$CFG"
curl -sk --resolve "$H:443:127.0.0.1" -K "$CFG" -H 'Content-Type: application/json' -o /dev/null -w 'create repository: %{http_code}\n' \
-X POST "https://$H/api/v1/user/repos" -d '{"name":"demo","private":true}'
rm -f "$CFG"
create repository: 201
Push a first commit to it over HTTPS (git reads the password through a small helper script, again never from a command line):
H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
W=$(mktemp -d); sudo sed -n 's/^GITEA_ADMIN_PASSWORD=//p' /root/gitea-credentials.txt > "$W/pw"
printf '#!/bin/sh\ncase "$1" in Username*) echo gitea_admin ;; *) cat "%s/pw" ;; esac\n' "$W" > "$W/askpass"; chmod 700 "$W/askpass"
git -C "$W" init -q -b main demo && echo "# demo" > "$W/demo/README.md"
git -C "$W/demo" add README.md && git -C "$W/demo" -c user.name=Admin -c user.email=admin@example.com commit -qm "first commit"
GIT_ASKPASS="$W/askpass" git -C "$W/demo" -c http.sslVerify=false -c "http.curloptResolve=$H:443:127.0.0.1" push -q "https://$H/gitea_admin/demo.git" main && echo "pushed over HTTPS"
rm -rf "$W"
pushed over HTTPS
Then add an SSH key for gitea_admin and clone the repository back over SSH on port 2222:
H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
W=$(mktemp -d); ssh-keygen -q -t ed25519 -N '' -C demo -f "$W/id"
sudo sed -n 's/^GITEA_ADMIN_PASSWORD=\(.*\)/user = "gitea_admin:\1"/p' /root/gitea-credentials.txt > "$W/cfg"
curl -sk --resolve "$H:443:127.0.0.1" -K "$W/cfg" -H 'Content-Type: application/json' -o /dev/null -w 'add SSH key: %{http_code}\n' \
-X POST "https://$H/api/v1/user/keys" -d "{\"title\":\"demo\",\"key\":\"$(cat "$W/id.pub")\"}"
GIT_SSH_COMMAND="ssh -i $W/id -o HostName=127.0.0.1 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$W/known_hosts" \
git clone -q "ssh://git@$H:2222/gitea_admin/demo.git" "$W/demo" && echo "cloned over SSH: $(cat "$W/demo/README.md")"
rm -rf "$W"
add SSH key: 201
cloned over SSH: # demo
The first time you connect over SSH from your workstation, compare the host key fingerprint with the one your instance generated:
sudo ssh-keygen -lf /var/lib/gitea/data/ssh/gitea.ed25519.pub
256 SHA256:<fingerprint of your instance> no comment (ED25519)
Step 7: Users, organisations and administration
As gitea_admin, open the user menu and Site Administration. The dashboard shows maintenance operations and system status; Identity & Access, User Accounts, Create User Account adds a user (they can change the password you give them at first sign in). Organisations and teams are created from +, New Organization.

To let people register their own accounts instead, set DISABLE_REGISTRATION = false (and SHOW_REGISTRATION_BUTTON = true) in the [service] section of /etc/gitea/app.ini. To let visitors browse public repositories without signing in, set REQUIRE_SIGNIN_VIEW = false. Edit the file with sudo nano /etc/gitea/app.ini, then restart:
sudo systemctl restart gitea
Step 8: Security checks
Port 80 sends browsers to HTTPS, nothing is visible without signing in, a visitor cannot register, and nginx closes the connection for a host name the instance does not serve (000 means no answer):
H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'sign in page: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/user/login"
curl -sk -o /dev/null -w 'explore without signing in: %{http_code} -> %{redirect_url}\n' --resolve "$H:443:127.0.0.1" "https://$H/explore/repos"
curl -sk -o /dev/null -w 'self registration: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST "https://$H/user/sign_up" \
-d 'user_name=visitor&email=visitor%40example.com&password=Visitor12345x&retype=Visitor12345x'
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
sign in page: 200
explore without signing in: 303 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/user/login?redirect_to=%2Fexplore%2Frepos
self registration: 403
unknown host name: 000
Gitea refuses to fetch from private and metadata addresses. A migration that asks it to import from the instance metadata service is refused:
H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
CFG=$(mktemp)
sudo sed -n 's/^GITEA_ADMIN_PASSWORD=\(.*\)/user = "gitea_admin:\1"/p' /root/gitea-credentials.txt > "$CFG"
curl -sk --resolve "$H:443:127.0.0.1" -K "$CFG" -H 'Content-Type: application/json' -w ' (HTTP %{http_code})\n' \
-X POST "https://$H/api/v1/repos/migrate" -d '{"clone_addr":"http://169.254.169.254/latest/meta-data/","repo_name":"metadata-test","service":"git"}'
rm -f "$CFG"
{"message":"You can not import from disallowed hosts.","url":"https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/api/swagger"} (HTTP 422)
The settings behind this: Gitea listens on loopback, builds its links from its configured address only, trusts forwarded client addresses from nginx on 127.0.0.1 only, keeps the installer locked, blocks outbound requests to non public addresses (EGRESS_MODE = lax with no allow list) and keeps registration off with sign in required:
sudo grep -E '^(HTTP_ADDR|PUBLIC_URL_DETECTION|SSH_PORT|INSTALL_LOCK|REVERSE_PROXY_TRUSTED_PROXIES|EGRESS_MODE|BLOCKED_HOST_LIST|DISABLE_REGISTRATION|REQUIRE_SIGNIN_VIEW) ' /etc/gitea/app.ini
HTTP_ADDR = 127.0.0.1
PUBLIC_URL_DETECTION = never
SSH_PORT = 2222
INSTALL_LOCK = true
REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.1/32
EGRESS_MODE = lax
EGRESS_MODE = lax
BLOCKED_HOST_LIST = 169.254.0.0/16
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
The secrets were generated on this instance and live in their own files, readable by root and the git user only:
sudo ls -l /etc/gitea/secrets | awk 'NR>1 {print $1, $3, $4, $NF}'
-rw-r----- root git internal_token
-rw-r----- root git jwt_secret
-rw-r----- root git lfs_jwt_secret
-rw-r----- root git secret_key
Gitea runs unprivileged in a sandbox, and neither Gitea nor nginx can reach the instance metadata service:
systemctl show gitea -p User -p ProtectSystem -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
User=git
ProtectSystem=strict
NoNewPrivileges=yes
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
The certificate was generated on this instance and names its own addresses:
H=$(sudo sed -n 's#^GITEA_URL=https://\([^/]*\)/.*#\1#p' /root/gitea-credentials.txt)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
Step 9: Your own DNS name and certificate
Point a DNS record at the instance, then set the name Gitea is served as. This mints a new self signed certificate for the name, updates nginx, Gitea's DOMAIN, ROOT_URL and SSH_DOMAIN (so clone URLs use it) and the URL in /root/gitea-credentials.txt, restarts Gitea, and keeps the name across reboots:
sudo gitea-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running gitea-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/gitea.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/gitea.key
sudo nginx -t && sudo systemctl reload nginx
Step 10: Your data, backups and upgrades
Repositories, LFS objects, attachments, packages and the SQLite database live under /var/lib/gitea/data:
sudo ls /var/lib/gitea/data
actions_artifacts
actions_log
attachments
avatars
gitea-repositories
gitea.db
home
indexers
jwt
lfs
packages
queues
repo-archive
repo-avatars
sessions
ssh
tmp
Back up /var/lib/gitea and /etc/gitea (the settings and the secrets files) together, or snapshot the root volume. Gitea's own gitea dump command writes a single archive of the database, repositories and settings; run it as the git user and keep a copy of /etc/gitea/secrets with it:
sudo -u git /usr/local/bin/gitea dump --config /etc/gitea/app.ini --file /var/lib/gitea/gitea-dump.zip
Ubuntu's unattended upgrades keep the operating system, git and nginx patched. To move to a newer Gitea release later, snapshot the instance, download the new gitea-<version>-linux-amd64 binary from the Gitea releases page, check it against the published .sha256, replace /usr/local/bin/gitea and run sudo systemctl restart gitea (Gitea migrates the database on start). Read the release notes first for breaking changes.
Step 11: Server components
| Component | Path |
|---|---|
| Gitea binary | /usr/local/bin/gitea |
| Settings | /etc/gitea/app.ini |
| Secrets (generated on first boot) | /etc/gitea/secrets/ |
| Repositories | /var/lib/gitea/data/gitea-repositories |
| Database (SQLite) | /var/lib/gitea/data/gitea.db |
| LFS objects | /var/lib/gitea/data/lfs |
| Gitea SSH host keys | /var/lib/gitea/data/ssh/ |
| Logs | journalctl -u gitea |
| Service units | /etc/systemd/system/gitea.service, /etc/systemd/system/gitea-firstboot.service |
| nginx site | /etc/nginx/sites-available/cloudimg-gitea |
| Certificate and key | /etc/nginx/ssl/ |
| Host name helper | /usr/local/sbin/gitea-set-hostname |
| Credentials | /root/gitea-credentials.txt (mode 0600) |
Step 12: Security recommendations
- Allow ports 22, 80, 443 and 2222 in the security group from your own networks only.
- Keep registration off and sign in required unless you need them, and create accounts for your team as the administrator.
- Keep the administrator password secret and change it if it may have been shared (Step 5); consider two factor authentication (user menu, Settings, Security).
- Leave the outbound request settings as shipped. If a webhook or mirror must reach a host on your private network, allow that one address in
[security] ALLOWED_HOST_LIST(and[migrations] ALLOWED_HOST_LISTfor migrations), never169.254.0.0/16. - Use your own DNS name and a certificate your browsers and git clients trust (Step 9).
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. - Back up
/var/lib/giteaand/etc/gitearegularly (Step 10).
Support and licensing
Gitea is open source under the MIT licence (the licence text is at /usr/share/doc/gitea/LICENSE); there is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA