SV Premium

Migration guides

Migrate Linux servers from AWS EC2 to Hetzner Cloud

Check your AWS dependencies, build the Hetzner network and servers, copy data, and switch traffic. Plan database rollback separately from DNS.

From AWS EC2 to Hetzner Cloud (dedicated vCPU) · Published 2026-10-08

Before you commit

Hands-on effort
1–2 weeks for stateless apps; 1–3 months when databases and object storage move too
Elapsed time
Add a pilot, data-copy and replication windows, plus at least two weeks of rollback capacity.
Examples cover
AWS CLI v2 · Ubuntu 24.04 · hcloud CLI
Content review
2026-10-09
Integration testing
Not recorded. Rehearse the commands and rollback on staging.

Prerequisites

  • An AWS dependency and commitment inventory, Hetzner capacity, staging and infrastructure administration.
  • Verified backups and a rehearsed data cutover and rollback procedure for every stateful service.

Don’t migrate yet if…

  • Required regions, managed services, IAM controls or availability guarantees have no acceptable replacement.
  • You cannot reconcile writes made after the database or storage cutover.

On AWS EC2 in us-east-1, an m7i.xlarge (4 vCPU, 16 GB) costs $150.82 a month on demand, before EBS disk ($0.08/GB-month for gp3) and egress ($0.09/GB after the first 100 GB). A Hetzner Cloud CCX23 with the same vCPU count and RAM costs $102.09 in Germany or Finland, including 160 GB of local NVMe and 20 TB of traffic. At 8 vCPU, it’s $297.99 on AWS against $163.59 on Hetzner.

Egress makes the biggest difference: 5 TB a month costs about $441 on AWS and nothing extra on Hetzner in the EU. Hetzner raised prices twice in 2026, so the gap is smaller than it used to be. Matching vCPU and RAM counts doesn’t guarantee the same performance. Compare your capacity and traffic in the calculator.

Before moving a VM, list the AWS services it calls, the data it needs and the network rules that protect it. Replacing those dependencies often takes longer than rebuilding the server.

0. Before you start

aws ce get-cost-and-usage --time-period Start=2026-09-01,End=2026-10-01 \
  --granularity MONTHLY --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=SERVICE

If EC2 is most of the bill, this guide fits. If RDS, S3, Lambda or managed Kafka dominate, moving the instances alone saves less than you’d think.

aws ec2 describe-instances --region us-east-1 \
  --filters Name=instance-state-name,Values=running \
  --query 'Reservations[].Instances[].[InstanceId,InstanceType,Placement.AvailabilityZone,PrivateIpAddress,Tags[?Key==`Name`]|[0].Value]' \
  --output table

aws ec2 describe-volumes --region us-east-1 \
  --query 'Volumes[].[VolumeId,Size,VolumeType,Attachments[0].InstanceId]' --output table

1. Pick a location

Location Code Network zone Included traffic per CCX server
Falkenstein, Nuremberg (DE) fsn1, nbg1 eu-central 20–60 TB
Helsinki (FI) hel1 eu-central 20–60 TB
Ashburn, VA (US) ash us-east 1–8 TB
Hillsboro, OR (US) hil us-west 1–8 TB
Singapore sin ap-southeast 1–8 TB

The EU locations are the cheapest and include the most traffic. US and Singapore servers cost a little more and include far less traffic, so check the current figures if you serve a lot of data from there. Private Networks only span one network zone, so a database in nbg1 and app servers in ash can’t share a private network. Traffic between network zones is billed as outgoing traffic. If your users are in the US and your EC2 is in us-east-1, ash keeps latency about the same.

2. Map the AWS pieces

AWS Hetzner Cloud
m7i.large / xlarge / 2xlarge / 4xlarge / 8xlarge CCX13 / CCX23 / CCX33 / CCX43 / CCX53 (same vCPU and RAM); CCX63 has 48 vCPU, 192 GB
EBS volume Local NVMe on the server, or Volumes (network block storage, 10 GB–10 TB)
Security group Cloud Firewall (stateful, inbound/outbound rules, applied to servers or labels)
VPC and subnets Network with subnets, one network zone
ELB / ALB / NLB Load Balancer (TCP, HTTP/HTTPS with managed certificates, health checks)
Elastic IP Primary IP (bound to one server) or Floating IP (move it between servers)
AMI Snapshot (hcloud server create-image --type snapshot)
Auto Scaling group None built in. Use Terraform/OpenTofu, Kubernetes with the cluster autoscaler, or Nomad
RDS Self-run PostgreSQL/MySQL on CCX servers, or a managed database from another provider
S3 Object Storage (S3-compatible, fsn1, nbg1, hel1 only), or keep S3
Route 53 Keep it, or move zones to Hetzner DNS
IAM API tokens per project (read or read/write), console roles per project member
CloudWatch Your own monitoring: Prometheus + node_exporter, or a hosted service

Hetzner Cloud has no equivalent of instance profiles. Anything on the new servers that still calls AWS needs an IAM user with a narrow policy and access keys stored as secrets. Rotate them, or use IAM Roles Anywhere if you already run a PKI.

3. Build the network and firewall

Install the hcloud CLI, create a project in the Hetzner Console, generate a read/write API token for it, and run hcloud context create prod.

hcloud ssh-key create --name ops --public-key-from-file ~/.ssh/id_ed25519.pub

hcloud network create --name prod --ip-range 10.10.0.0/16
hcloud network add-subnet prod --type cloud --network-zone eu-central --ip-range 10.10.1.0/24

hcloud firewall create --name web
hcloud firewall add-rule web --direction in --protocol tcp --port 443 \
  --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule web --direction in --protocol tcp --port 22 \
  --source-ips 203.0.113.10/32

Rebuild security groups rule by rule (aws ec2 describe-security-groups). Hetzner firewalls can’t reference other firewalls the way security groups reference each other, so use the private subnet’s CIDR for internal traffic. Traffic over the private Network isn’t filtered by Cloud Firewalls, so use a host firewall (nftables, ufw) if you need rules between your own servers.

4. Provision the servers

hcloud server create --name app-1 --type ccx23 --image ubuntu-24.04 \
  --location nbg1 --ssh-key ops --network prod --firewall web \
  --user-data-from-file cloud-init.yaml --label role=app

Reuse your EC2 user data if you have it. Hetzner servers run cloud-init too, but there is no instance metadata service with IAM credentials, and scripts that call 169.254.169.254 for AWS-specific data will fail. A minimal cloud-init.yaml:

#cloud-config
package_update: true
packages: [nginx, prometheus-node-exporter]
users:
  - name: deploy
    groups: sudo
    shell: /bin/bash
    ssh_authorized_keys: ["ssh-ed25519 AAAA... deploy"]

For anything beyond a few servers, use the hetznercloud/hcloud provider in Terraform or OpenTofu. It covers servers, networks, firewalls, load balancers, volumes and Primary IPs. A load balancer for the app tier:

hcloud load-balancer create --name web --type lb11 --location nbg1
hcloud load-balancer attach-to-network web --network prod
hcloud load-balancer add-target web --label-selector role=app --use-private-ip
hcloud load-balancer add-service web --protocol tcp --listen-port 443 --destination-port 443

5. Connect AWS and Hetzner during the move

During the transition, app servers on Hetzner will talk to databases and caches still on AWS. Don’t open those to the internet. Run WireGuard (or Tailscale, which uses it) between a small gateway in your VPC and one on Hetzner, and route the VPC CIDR and 10.10.0.0/16 through it. Choose non-overlapping ranges before you build anything. Expect a few milliseconds of latency between us-east-1 and ash, and around 80–100 ms to Germany. Chatty database access over that link will be slow, so move each app together with its data.

6. Move the data

Files and volumes: rsync over SSH, run once in bulk and again just before cutover:

rsync -aHAX --numeric-ids --delete --info=progress2 \
  /srv/data/ deploy@hetzner-app-1:/srv/data/

PostgreSQL: for near-zero downtime, replicate instead of dump-and-restore. From self-managed PostgreSQL on EC2, set up streaming replication (pg_basebackup -R on the new server). From RDS, which doesn’t allow physical replication out, use logical replication. Set rds.logical_replication = 1 in the parameter group (this needs a reboot), grant the replication user rds_replication, then:

-- on RDS
CREATE PUBLICATION migrate FOR ALL TABLES;
-- on Hetzner, after loading the schema with pg_dump --schema-only
CREATE SUBSCRIPTION migrate CONNECTION 'host=... dbname=app user=repl password=...'
  PUBLICATION migrate;

Logical replication doesn’t copy sequences or DDL. Before cutover, stop writes, set each sequence past its current maximum, then point the apps at the new primary.

S3: copy with rclone, configured with an s3 remote for AWS and one for Hetzner (provider = Other, endpoint nbg1.your-objectstorage.com). Check Hetzner’s list of supported S3 actions first if you use lifecycle rules, event notifications or object lock.

rclone sync aws:my-bucket hetzner:my-bucket --checksum --transfers 32 --progress

Egress during the migration. AWS bills data leaving AWS at $0.09/GB for the first 10 TB a month, so copying 20 TB costs roughly $1,750. AWS waives these charges for customers moving off AWS, if you ask first. Open a support case saying the request is for free data transfer to move off AWS. AWS reviews it per account and gives credits, and you then have 60 days to finish the move. It covers data transfer out only, not CloudFront or other services, and you don’t have to close the account.

7. Verify

8. Cut over

  1. A day ahead, lower the TTL on the records you’ll change to 60 seconds.
  2. If you use weighted routing in Route 53, send 5–10% of traffic to the Hetzner load balancer first, then increase it.
  3. For stateful services: stop writes, wait for replication to catch up, promote the Hetzner database, then switch DNS.
  4. Watch error rates and latency on both sides until traffic to AWS stops.

9. What you give up

Agree on these trade-offs with your team before you start:

10. Rollback plan

Keep the AWS app stack ready to restart for at least two weeks. For stateless services, rolling back means restarting that stack and pointing DNS back at it. Clients follow as cached records expire, which is why the TTL is lowered in step 8.

Database rollback is a separate procedure. Switching DNS back doesn’t copy writes back. Before cutover, choose and rehearse one of two approaches: reverse replication (or CDC) from Hetzner to AWS, or a write freeze followed by a controlled copy-back. The subscription above only replicates one way. Reverse logical replication needs its own publication, subscription, credentials and network path, plus handling for sequences, DDL and replication loops, and the old database has to stay online as its target. Never leave both sides writable without a conflict strategy.

At rollback, pause writes, reconcile new data, verify replication or copy-back completion, then switch the apps and resume writes on only one primary. Apply the same plan to changed objects and files. If you cannot rehearse this within your recovery objective, migrate stateless services first and leave the data in place.

11. Clean up

Found a command or version mismatch? Report a correction with the version and steps to reproduce it. Remove secrets and customer data first.