May 12, 2026

Self-hosting Gitea on AWS with Terraform

How I provisioned an AWS EC2 instance with Terraform, configured SSH access, installed Gitea, connected MariaDB, and put Nginx in front as a reverse proxy.

Quick navigation

Repository: github.com/kaungmyathan18/devops-private-git-server

Overview

I recently set up a private Git server using Gitea on an AWS EC2 instance. The stack is simple:

  • AWS EC2 running Amazon Linux 2023
  • Terraform for infrastructure provisioning
  • MariaDB as the Gitea database
  • Nginx as a reverse proxy
  • Gitea running on port 3000
  • Public access through port 80

The final flow looks like this:

Browser -> Nginx :80 -> Gitea :3000 -> MariaDB

Infrastructure with Terraform

The Terraform project provisions a VPC, subnets, security group, EC2 instance, and an AWS key pair.

The EC2 instance uses Amazon Linux 2023:

data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

I originally tried t2.micro, but AWS rejected it because it was not Free Tier eligible for my account. I switched to:

instance_type = "t3.micro"

SSH key setup

Instead of manually creating an EC2 key pair in the AWS Console, Terraform creates one from my local public SSH key:

create_key_pair = true
key_name        = "kmh-admin"
public_key_path = "~/.ssh/id_ed25519.pub"

Only the public key is used. The private key stays local.

To connect:

ssh -i ~/.ssh/id_ed25519 ec2-user@YOUR_PUBLIC_IP

Database setup

Gitea uses MariaDB. The database setup script looks like this:

CREATE DATABASE `gitea` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'gitea'@'localhost' IDENTIFIED BY 'gitea';
GRANT ALL PRIVILEGES ON `gitea`.* TO 'gitea'@'localhost';
FLUSH PRIVILEGES;
EXIT;

One mistake I made was using:

GRANT ALL PRIVILEGES ON DATABASE gitea.* TO gitea@localhost;

MariaDB does not use ON DATABASE in that syntax. The correct form is:

GRANT ALL PRIVILEGES ON `gitea`.* TO 'gitea'@'localhost';

Gitea systemd service

I created a systemd service for Gitea:

[Unit]
Description=Gitea Self-Hosted Git Server
After=network.target mariadb.service

[Service]
User=gitea
Group=gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
WorkingDirectory=/var/lib/gitea
Environment=USER=gitea HOME=/var/lib/gitea

[Install]
WantedBy=multi-user.target

The file must live here:

/etc/systemd/system/gitea.service

Then enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now gitea
sudo systemctl status gitea

A small issue I hit: I wrote Execstart instead of ExecStart. Systemd is case-sensitive, so the service failed until I fixed the capitalization.

Nginx reverse proxy

Gitea listens on port 3000, but users should access it through normal HTTP on port 80.

The Nginx config:

server {
    listen 80;
    server_name _;

    client_max_body_size 512M;

    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;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;

        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

On Amazon Linux, I placed it at:

/etc/nginx/conf.d/gitea.conf

Then tested and reloaded Nginx:

sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx

Common issues I hit

AWS credentials expired

Terraform failed with:

ExpiredToken: The security token included in the request is expired

The fix was to log in again and export credentials:

aws login
eval "$(aws configure export-credentials --format env)"
terraform apply

SSH was hanging

SSH timed out because the security group initially allowed port 22 only from inside the VPC.

I fixed it by allowing my public IP:

allowed_ssh_cidr_blocks = ["YOUR_PUBLIC_IP/32"]

Port 80 was timing out

Nginx was running, but AWS security group rules did not allow public HTTP traffic.

I opened HTTP and HTTPS publicly:

allowed_service_cidr_blocks = ["0.0.0.0/0"]

I kept application ports private by separating them:

allowed_application_cidr_blocks = ["10.0.0.0/16"]

Gitea redirected to the wrong URL

When clicking register, Gitea redirected to something like:

http://user/sign_up

The issue was a bad ROOT_URL and incorrect domain values in /etc/gitea/app.ini.

Bad config:

DOMAIN = http://44.203.218.46/
SSH_DOMAIN = http://44.203.218.46/
ROOT_URL = http://44.203.218.46//

Correct config:

DOMAIN = 44.203.218.46
SSH_DOMAIN = 44.203.218.46
ROOT_URL = http://44.203.218.46/

After updating the config:

sudo systemctl restart gitea
sudo systemctl reload nginx

Final result

After the fixes, Gitea was reachable through Nginx:

http://YOUR_PUBLIC_IP

The setup now has:

  • Terraform-managed AWS infrastructure
  • EC2 key pair created from a local SSH public key
  • MariaDB-backed Gitea
  • Gitea managed by systemd
  • Nginx reverse proxy on port 80
  • Correct ROOT_URL configuration for web redirects

This is a simple but solid starting point for a self-hosted private Git server. Terraform and configs on GitHub