楚成王見形勢不利,退回申地(今河南南陽)駐紮,防備秦國的襲擊,又命令戍守齊國穀邑的申公叔時和圍攻宋國的子玉率部撤退,避免與晉軍交戰。他認為晉文公在外流亡了十九年,險阻艱難,全都經曆了:民情真假,他都知道了,上天使他享有高年,同時除掉他的禍患。天所予,不可敗。但是子玉卻驕傲自負,堅持要與晉軍作戰,他說:雖不敢保證一定能建立什麼了不起的戰功,但希望用勝利堵住奸邪小人(指楚大夫蒍賈)進讒言的嘴。於是,子玉派大夫宛春到晉軍中談判,條件是恢複衛侯的君位,同時退還曹國的土地,楚軍解除對宋國的包圍。狐偃認為子玉太無禮,晉君(文公)隻得到釋宋之圍一項好處,而楚臣(子玉)卻得到恢複曹、衛兩項好處。不能失掉這個戰機。先軫不同意,他認為楚國一句話就使曹、衛複國、宋解圍,三個國家都安全,晉國如果不同意,這三國就均被滅亡,這就是晉國無禮。不如暗中答應恢複曹、衛兩國,使他們叛離楚國;再用扣留楚軍使者宛春的辦法來激怒子玉,等打完仗再考慮曹、衛的問題。晉文公很高興,就采納了先軫的意見。曹、衛兩國果然派人到子玉營中同楚斷交。子玉十分氣憤,立即率軍北上與晉軍作戰。文公見楚軍逼近,下令退避三舍(古時一日行軍30裏為一舍)。將士們對後退很不理解,認為晉君躲避楚臣是極大的恥辱,何況楚軍攻宋不下,在外轉戰多時,已經疲憊不振。狐偃向他們解釋這樣做是為了報答文公當年逃亡時楚君給予的恩惠,兌現文公當年所許的“兩國若交兵,退避三舍相報”的諾言。於是晉文公、宋成公、齊國大夫國歸父、崔夭、秦穆公之子小子愁率軍退後九十裏,在衛國的城濮(今山東濮縣南)駐紮下來。楚軍隨即逼了上來,在城濮附近的險要地帶紮營。
晉文公既害怕別人議論自己忘恩,又擔心士卒不願盡力作戰,所以在與楚交戰的問題上猶豫不決。三軍將領都勸他下決心打。狐偃認為,這一仗若打勝,就可以得到諸侯國的擁戴,取得霸主的地位;即便打不勝,晉國外有黃河之阻,內有太行之險,沒什麼可擔憂的。欒枝也說,漢水北麵的姬姓國都被楚國吞並,思念小恩小惠而忘記大恥大辱,於國不利,應當與楚國交戰!文公這才堅定了決心。
子玉誤把晉軍禮讓性的後撤誤認為是害怕楚軍,於是剛紮下營盤便派大夫鬥勃向晉文公挑戰道:“請和您的部下遊戲一番,您可以扶著車前橫木觀賞,我也陪您來開開眼界。”文公讓欒枝回答說:我們國君領教命令了。楚王的恩惠不敢忘記。既然得不到諒解,那就煩請大夫告訴你們的將帥:準備好你們的戰車,敬奉你們的君命,明天早晨戰場上相見!
晉楚城濮大戰前,晉軍方麵,有戰車700乘,兵員37000人,另有齊、秦、宋的支援。中軍元帥是先軫,卻溱為副;上軍主將是狐毛,狐偃為副;下軍主將是欒枝,胥臣為副。楚軍方麵,子玉為中軍主帥,指揮警衛王室的西廣、東宮及若教六卒,共有戰車180乘;子上為右軍主將,指揮陳、蔡兩國的軍隊;子西為左軍主將,指揮申、息兩地的地方部隊。晉軍的上軍對楚軍的左軍,下軍對右軍,臨戰,子玉誇下海口說:“今天晉軍必定覆沒!”
戰鬥開始,晉下軍副將胥臣命令士卒把駕車的馬蒙上虎皮,首先向楚右軍發起攻擊,陳、蔡的軍隊跟從楚軍本是不得已,遭到這一突然進攻,立即潰不成軍,蔡國公子印也被殺死,晉上軍主將狐毛另設前軍兩隊,出擊楚軍的逃兵,楚軍右翼被徹底打垮;晉下軍主將欒枝讓士卒砍伐木柴拖在車後,揚起塵土,偽裝敗逃,楚中軍立即發起追擊,左軍主將子西求勝心切,以為晉軍主力潰逃,帶部率先追趕,造成楚軍側翼空虛。晉見楚中計,元帥先軫率領中軍精銳攔腰截擊,狐毛、狐偃反轉回頭殺來,前後夾擊,楚國的左軍也被打垮。子玉見左右兩軍全垮,急忙收兵,這才不至於全軍覆滅。
城濮之戰以晉勝楚敗而告結束。晉軍在楚營內歇兵三天,班師而歸。向周天子獻上俘獲的戰車100輛和俘虜的步兵1000人。周天子設宴款待晉文公,命令大臣尹氏、王子虎和內史叔興父用策書頒命晉文公為諸侯之長,並賞賜了文公許多財物。