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 and a MySQL cutover through replication.
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
GCP production writes to MySQL until the very last day, so a plain dump/restore would have meant a long outage. The cutover relies on replication instead:
- Initial copy of the database (dump with GTID position).
- The OVH MySQL becomes a replica of GCP production. The connection goes from OVH to GCP, filtered on OVH’s egress IP and encrypted with TLS. The OVH database stays in
super_read_onlyso nothing can write to it by mistake. - On cutover day: production goes read-only, wait for lag to hit zero, promote the replica, then switch DNS (TTL lowered a few days before).
Production is only down while the replica catches up, which takes a few minutes.