GCP → OVH Migration: Symfony Extranet
Migration of a Symfony extranet and its showcase site from GCP to a dedicated OVH Public Cloud project. Infrastructure in Terraform, apps on Docker Compose behind Traefik, GitHub Actions deployment, production cutover in 40 minutes.
- Context
- A Symfony extranet and its showcase site hosted on GCP, to be moved to OVH in a French region without breaking production.
- My role
- Infrastructure written in Terraform, Docker Compose stack, GitHub Actions deployment, then the production cutover.
- Outcome
- Production runs on OVH. Full cutover in 40 minutes: database export, import, DNS switch.
The project
The extranet (Symfony) and its showcase site were running on GCP. They had to move to OVH, in a French region, without breaking production.
All the Terraform is written by hand, without an off-the-shelf module, to know exactly what each resource creates.
Architecture
A dedicated OVH project
At first, the infrastructure was going into PharmaGDD’s existing Public Cloud project. It ended up in its own OVH project: separate OpenStack and API credentials, separate billing, and a mistake on this project can’t touch the main infrastructure.
This took a bit more work at the start: new credentials, registry and S3 access to set up again.
Private network + bastion
The application VM sits in a dedicated private network (vRack/VLAN), separate from PharmaGDD’s. SSH access only goes through a bastion. In the security group, only 80 and 443 are open to the public, and 22 is only open from the bastion.
Internet ──► Traefik (80/443) ──► FrankenPHP (Symfony extranet)
└─► SPA container (showcase site)
Admin ──► Bastion ──SSH──► App VM (private network)
Terraform
The Terraform lives in its own repo. The ovh provider handles the project, network and DNS; OpenStack handles instances and security groups. You have to follow the network → subnet → instance order, and know what depends on the OVH API and what depends on OpenStack.
The VM is then set up (Docker, Compose, user, /opt/... layout) by a script run over SSH through the bastion.
Application stack
Everything runs on Docker Compose on the VM:
- Traefik for TLS and label-based routing
- FrankenPHP to serve the Symfony extranet
- the showcase site (SPA) in its own container. An S3 bucket updated by CI was the first plan, dropped to keep a single entry point.
- MySQL and Redis
- backups to an OVH S3 bucket
CI/CD
Deployment goes through GitHub Actions, with a self-hosted runner shared at the organization level. There’s no runner on the production VM.
The data cutover
Replication, studied then dropped
GCP production writes to MySQL until the very last day. My first proposal was to use replication: initial copy with a GTID position, the OVH database as a replica of GCP production, then promote the replica on cutover day. The outage would have been limited to the catch-up time.
I studied whether it was feasible. The two MySQL servers sat on two different networks: for the OVH replica to read GCP production, the MySQL port had to be opened to the public, with TLS and IP filtering, all on the production database. Too many constraints and too much exposure to save a few dozen minutes of downtime.
What we did
A simpler, one-shot cutover:
- Back up and export the GCP production database.
- Import the export into the OVH MySQL.
- Switch DNS to OVH.
The whole thing took 40 minutes.
What I take away
The most elegant solution was not the right one here. For an extranet, 40 minutes of planned downtime beat a production database exposed to the internet for the length of the migration. The study still paid off: it let us decide on concrete arguments rather than gut feeling.